package org.jtb.quickr;

import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.Date;
import org.apache.log4j.Level;
import org.apache.log4j.Logger;
import org.jdom.Document;
import org.jdom.Element;
import org.jdom.JDOMException;
import org.jdom.input.SAXBuilder;

/**
 * Abstract base class for all Flickr response classes.
 */
public abstract class FlickrResponse {
    /**
     * Flickr response status.
     */
    public static class Status {
        public static final Status OK = new Status("ok");
        public static final Status FAIL = new Status("fail");
        
        private String statusString;
        
        Status(String statusString) {
            this.statusString = statusString;
        }
        
        public String toString() {
            return statusString;
        }
        
        public boolean equals(Object o) {
            Status s = (Status)o;
            return toString().equals(s.toString());
        }
    }
    
    /**
     * Flickr error code. This class encapsulates many of the error codes returned
     * from the Flickr services.
     */
    public static class ErrorCode {
        public static final ErrorCode USER_NOT_FOUND = new ErrorCode("1");
        public static final ErrorCode INVALID_SIGNATURE = new ErrorCode("96");
        public static final ErrorCode INVALID_AUTH_TOKEN = new ErrorCode("98");
        public static final ErrorCode INSUFFICIENT_PERMISSION = new ErrorCode("99");
        public static final ErrorCode INVALID_API_KEY = new ErrorCode("100");
        public static final ErrorCode SERVICE_UNAVAILABLE = new ErrorCode("105");
        public static final ErrorCode FORMAT_NOT_FOUND = new ErrorCode("111");
        public static final ErrorCode METHOD_NOT_FOUND = new ErrorCode("112");
        
        private String code;
        
        ErrorCode(String code) {
            this.code = code;
        }
        
        public String toString() {
            return code;
        }
        
        public boolean equals(Object o) {
            ErrorCode ec = (ErrorCode)o;
            return toString().equals(ec.toString());
        }
    }
   
    private static Logger logger = Logger.getLogger(FlickrResponse.class.getName());
    
    /**
     * Construct a new, empty object.
     */
    protected FlickrResponse() {
        // nothing, sub class must manually init
    }
    
    /**
     * Construct a new object from the given stream.
     * The stream should contain the XML representation of the
     * response object.
     * @param is The stream.
     * @throws QuickrException If a problem occurs.
     */
    protected FlickrResponse(InputStream is) throws QuickrException {
        if (is == null) {
            throw new NullPointerException("stream can not be null");
        }
        init(is);
    }
    
    /**
     * Construct a new response object from the given element.
     * The element should be to the root element a of Flickr response.
     * @param e The element.
     * @throws QuickrException If a problem occurs.
     */
    protected FlickrResponse(Element e) throws QuickrException {
        if (e == null) {
            throw new NullPointerException("element cannot be null");
        }
        populate(e);
    }
    
    /**
     * Same as FlickrReponse(InputStream), but allows base classes to defer
     * initialization.
     * @param is The stream.
     * @throws QuickrException If a problem occurs.
     */
    protected void init(InputStream is) throws QuickrException {
        try {
            SAXBuilder sb = new SAXBuilder();
            Document d = sb.build(is);
            Element e = d.detachRootElement();
            
            checkStatus(e);
            populate(e);
        } catch (JDOMException je) {
            throw new QuickrException("could not build response document", je);
        } catch (IOException ioe) {
            throw new QuickrException("could not build response document", ioe);
        }
    }
    
    private void checkStatus(Element e) throws FlickrResponseException {
        Status status = new Status(e.getAttributeValue("stat"));
        if (!status.equals(Status.OK)) {
            Element errElement = e.getChild("err");
            if (errElement == null) {
                throw new FlickrResponseException("flickr error", status);
            } else {
                ErrorCode errorCode = new ErrorCode(errElement.getAttributeValue("code"));
                String errorMessage = errElement.getAttributeValue("msg");
                
                throw new FlickrResponseException("flickr error", status, errorCode, errorMessage);
            }
        }
    }
    
    /**
     * Get tag body as a string. This is a convenience method for use by base classes.
     * @param parentElement The parent element.
     * @param tagName The tag name.
     * @return The tag's body content.
     */
    protected static String getBodyString(Element parentElement, String tagName) {
        Element childElement = parentElement.getChild(tagName);
        if (childElement == null) {
            return null;
        }
        return childElement.getTextTrim();
    }

    /**
     * Get tag body as a string. This is a convenience method for use by base classes.
     * @param element The element.
     * @return The element's content.
     */
    protected static String getBodyString(Element element) {
        if (element == null) {
            return null;
        }
        return element.getTextTrim();
    }

    /**
     * Get tag body as an int. This is a convenience method for use by base classes.
     * @param parentElement The parent element.
     * @param tagName The tag name.
     * @return The tag's body content.
     */
    protected static int getBodyInt(Element parentElement, String tagName) {
        Element childElement = parentElement.getChild(tagName);
        if (childElement == null) {
            return 0;
        }
        return Integer.valueOf(childElement.getTextTrim());
    }
    
    /**
     * Get an element attribute as a boolean. This is a convenience method for use
     * by base classes.
     * <p>
     * Flickr uses the values "0" and "1" to represent true and false
     * respectively.
     * @param e The element.
     * @param attrName The attribute name.
     * @return The attribute value as a boolean.
     */
    protected static boolean getAttrBoolean(Element e, String attrName) {
        String val = e.getAttributeValue(attrName);
        if (val == null) {
            return false;
        }
        BooleanInt bi = new BooleanInt(val);
        
        return bi.booleanValue();
    }
    
    /**
     * Get an element attribute as an int. This method is a convenience for use
     * by base classes.
     * @param e The element.
     * @param attrName The attribute name.
     * @return The attribute value as an int, or -1 if the attribute was not set.
     */
    protected static int getAttrInt(Element e, String attrName) {
        String val = e.getAttributeValue(attrName);
        if (val == null) {
            return 0;
        }
        return Integer.valueOf(val).intValue();
    }

    /**
     * Get an element attribute as a long. This method is a convenience for use
     * by base classes.
     * @param e The element.
     * @param attrName The attribute name.
     * @return The attribute value as a long, or -1 if the attribute was not set.
     */
    protected static long getAttrLong(Element e, String attrName) {
        String val = e.getAttributeValue(attrName);
        if (val == null) {
            return 0;
        }
        return Long.valueOf(val).longValue();
    }

    /**
     * Get an element attribute as a date. This method is a convenience for use
     * by base classes.
     * @param e The element.
     * @param attrName The attribute name.
     * @return The attribute value as an int, or -1 if the attribute was not set.
     */
    protected static Date getAttrDate(Element e, String attrName) {
        String val = e.getAttributeValue(attrName);
        if (val == null) {
            return null;
        }
        return new Date(Long.valueOf(val).longValue()*1000);
    }

    /**
     * Get an element attribute as a URL. This method is a convenience for use
     * by base classes.
     * @param e The element.
     * @param attrName The attribute name.
     * @return The attribute value as a URL, or null if 
     * the attribute was not set.
     */
    protected static URL getAttrUrl(Element e, String attrName) {
        String val = e.getAttributeValue(attrName);
        if (val == null) {
            return null;
        }
        try {
            return new URL(val);
        } catch (MalformedURLException mfue) {
            if (logger.isEnabledFor(Level.WARN)) {
                logger.warn("could not get attribute" + attrName + ", value: " + val + " as URL", mfue);
            }
            return null;
        }
    }
    
    /**
     * Populate the response object's fields based on the response element.
     * Subclasses must implement this method.
     * @param e The response element.
     * @throws QuickrException If a problem occurs populating the response object's fields.
     */
    protected abstract void populate(Element e) throws QuickrException;
}
